转https://www.jianshu.com/p/b0ddadd34037
由于傲娇的苹果在 iOS9 之后已经放弃了 NSURLConnection,所以在现在的实际开发中,除了大家常见的 AFN 框架,一般使用的是 iOS7 之后推出的 NSURLSession,作为一名 iOS 开发人员,如果你只知道 AFN 框架来进行网络请求,那就只能说是 too young too simple,sometimes naive。
目录
- NSURLSession 的优势
- NSURLSessionTask 的子类
- NSURLSessionDataTask 发送 GET 请求
- NSURLSessionDataTask 发送 POST 请求
- NSURLSessionDataTask 设置代理发送请求
- 设置代理之后的强引用问题
- NSURLSessionDataTask 简单下载
- NSURLSessionDownloadTask 简单下载
- dataTask 和 downloadTask 下载对比
- 写在最后
- 【补充】NSURLSession 详解离线断点下载的实现
NSURLSession 的优势
- NSURLSession 支持 http2.0 协议
- 在处理下载任务的时候可以直接把数据下载到磁盘
- 支持后台下载|上传
- 同一个 session 发送多个请求,只需要建立一次连接(复用了TCP)
- 提供了全局的 session 并且可以统一配置,使用更加方便
- 下载的时候是多线程异步处理,效率更高
NSURLSessionTask 的子类
NSURLSessionTask 是一个抽象类,如果要使用那么只能使用它的子类
NSURLSessionTask 有两个子类
NSURLSessionDataTask,可以用来处理一般的网络请求,如 GET | POST 请求等
- NSURLSessionDataTask 有一个子类为 NSURLSessionUploadTask,用于处理上传请求的时候有优势
NSURLSessionDownloadTask,主要用于处理下载请求,有很大的优势

NSURLSession 的子类
NSURLSessionDataTask 发送 GET 请求
发送 GET 请求的步骤非常简单,只需要两步就可以完成:
1. 使用 NSURLSession 对象创建 Task
2. 执行 Task
1 |
//确定请求路径
NSURL url = [NSURL URLWithString:@”http://120.25.226.186:32812/login?username=520&pwd=520&type=JSON“];
//创建 NSURLSession 对象
NSURLSession session = [NSURLSession sharedSession];
1 | /** |
1 | <h4 id='4'> NSURLSessionDataTask 发送 POST 请求 </h4> |
NSURLSessionDataTask 设置代理发送请求
创建 NSURLSession 对象设置代理
使用 NSURLSession 对象创建 Task
执行 Task
1
2
//确定请求路径
NSURL url = [NSURL URLWithString:@”http://120.25.226.186:32812/login“];
//创建可变请求对象
NSMutableURLRequest requestM = [NSMutableURLRequest requestWithURL:url];
//设置请求方法
requestM.HTTPMethod = @”POST”;
//设置请求体
requestM.HTTPBody = [@”username=520&pwd=520&type=JSON” dataUsingEncoding:NSUTF8StringEncoding];
//创建会话对象,设置代理
/
第一个参数:配置信息
第二个参数:设置代理
第三个参数:队列,如果该参数传递nil 那么默认在子线程中执行
/
NSURLSession session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self delegateQueue:nil];
//创建请求 Task
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM];
//发送请求
[dataTask resume];
1 | 4. 遵守协议,实现代理方法(常用的有三种代理方法) |
设置代理之后的强引用问题
NSURLSession 对象在使用的时候,如果设置了代理,那么 session 会对代理对象保持一个强引用,在合适的时候应该主动进行释放
可以在控制器调用 viewDidDisappear 方法的时候来进行处理,可以通过调用 invalidateAndCancel 方法或者是 finishTasksAndInvalidate 方法来释放对代理对象的强引用
invalidateAndCancel 方法直接取消请求然后释放代理对象
finishTasksAndInvalidate 方法等请求完成之后释放代理对象。
1
2
[self.session finishTasksAndInvalidate];
1
2
NSURLSessionDataTask 简单下载
在前面请求数据的时候就相当于一个简单的下载过程,NSURLSessionDataTask 下载文件具体的步骤与上类似:
1. 使用 NSURLSession 对象创建一个 Task 请求
2. 执行请求
1 |
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:
@”http://120.25.226.186:32812/resources/images/minion_01.png“]
completionHandler:^(NSData _Nullable data, NSURLResponse _Nullable response, NSError * _Nullable error) {
1 | //解析数据 |
1 | <h4 id='8'> NSURLSessionDownloadTask 简单下载 </h4> |
以上方法无法监听下载进度,如要获取下载进度,可以使用代理的方式进行下载。
dataTask 和 downloadTask 下载对比
- NSURLSessionDataTask
- 下载文件可以实现离线断点下载,但是代码相对复杂
- NSURLSessionDownloadTask
- 下载文件可以实现断点下载,但不能离线断点下载
- 内部已经完成了边接收数据边写入沙盒的操作
- 解决了下载大文件时的内存飙升问题
写在最后
关于使用 NSURLSession 进行上传文件操作,我只想说真的很麻烦,建议大家时间充沛且有兴趣的可以研究一下,如果不想研究也是可以的,继续使用我们伟大的 AFN 框架就好。至于 AFN 框架的使用,这里就不赘述了,后期如果有时间会更新一些常用的 AFN 使用方法,敬请期待。
附:使用 NSURLSession 上传文件主要步骤及注意点
主要步骤:
- 确定上传请求的路径( NSURL )
- 创建可变的请求对象( NSMutableURLRequest )
- 修改请求方法为 POST
- 设置请求头信息(告知服务器端这是一个文件上传请求)
- 按照固定的格式拼接要上传的文件等参数
- 根据请求对象创建会话对象( NSURLSession 对象)
- 根据 session 对象来创建一个 uploadTask 上传请求任务
- 执行该上传请求任务(调用 resume 方法)
- 得到服务器返回的数据,解析数据(上传成功 | 上传失败)
注意点:
创建可变的请求对象,因为需要修改请求方法为 POST,设置请求头信息
设置请求头这个步骤可能会被遗漏
要处理上传参数的时候,一定要按照固定的格式来进行拼接
需要采用合适的方法来获得上传文件的二进制数据类型( MIMEType,获取方式如下)
对着该文件发送一个网络请求,接收到该请求响应的时候,可以通过响应头信息中的 MIMEType 属性得到
使用通用的二进制数据类型表示任意的二进制数据 application/octet-stream
调用 C 语言的 API 来获取
1
2
[self mimeTypeForFileAtPath:@”此处为上传文件的路径”]
1
2
NSURLSessionDataTask 大文件离线断点下载
- 主要内容
- 实现文件下载
- 监听文件的下载进度
- 解决内存飙升问题
- 常用操作:开始 | 暂停 | 取消 | 恢复
- 断点下载
- 离线断点下载
- 实现源码
1. 实现文件下载
对于文件下载的实现这里就不再赘述,如果记不太清的话可以参考篇头提到的文章,里面有详细介绍,这里我就上代码了
1
2
//01 确定请求路径
NSURL URL = [NSURL URLWithString:@”http://sony.it168.com/data/attachment/forum/201410/20/2154195j037033ujs7cio0.jpg“];
//02 创建会话对象 设置代理
NSURLSession session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//03 创建请求 发送请求
[[session dataTaskWithURL:URL] resume];
1 | ###### 2. 监听文件的下载进度 |
做完以上两步之后就可以实现文件的下载操作和监听下载进度,但是此时会有很多问题,比如:内存飙升、下载进度错乱、无法控制下载状态等等,对于这些存在的问题,我们下面将一一进行解决。

初步实现效果(存在问题)
3. 解决内存飙升问题
产生的原因:在下载文件的过程中,系统会先把文件保存在内存中,等到文件下载完毕之后再写入到磁盘
解决方案:在下载文件时,一边下载一边写入到磁盘,减小内存使用
在 iOS 中常用的有两种方法可以实现:
- NSFileHandle 文件句柄
- NSOutputStream 输出流
方案一:NSFileHandle 文件句柄,大致分为四个步骤
对代理方法进行改良
1
2
-(void)URLSession:(NSURLSession )session dataTask:(nonnull NSURLSessionDataTask )dataTask
didReceiveResponse:(nonnull NSURLResponse )response
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
//接受到响应的时候 告诉系统如何处理服务器返回的数据
completionHandler(NSURLSessionResponseAllow);
//得到请求文件的数据大小
self.totalLength = response.expectedContentLength;
//拼接文件的全路径
NSString fileName = response.suggestedFilename;
NSString cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString fullPath = [cachePath stringByAppendingPathComponent:fileName];1
2
3
4//【1】在沙盒中创建一个空的文件
[[NSFileManager defaultManager] createFileAtPath:fullPath contents:nil attributes:nil];
//【2】创建一个文件句柄指针指向该文件(默认指向文件开头)
self.handle = [NSFileHandle fileHandleForWritingAtPath:fullPath];}
-(void)URLSession:(NSURLSession )session dataTask:(NSURLSessionDataTask )dataTask didReceiveData:(NSData )data {
//【3】使用文件句柄指针来写数据(边写边移动)
[self.handle writeData:data];
//累加已经下载的文件数据大小
self.currentLength += data.length;
//计算文件的下载进度 = 已经下载的 / 文件的总大小
self.progressView.progress = 1.0 self.currentLength / self.totalLength;
}
-(void)URLSession:(NSURLSession )session task:(NSURLSessionTask )task didCompleteWithError:(NSError *)error {
//【4】关闭文件句柄
[self.handle closeFile];
}1
2方案二:NSOutputStream 输出流,大致分为三个步骤
对代理方法的处理
1
2
-(void)URLSession:(NSURLSession )session dataTask:(nonnull NSURLSessionDataTask )dataTask
didReceiveResponse:(nonnull NSURLResponse )response
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
//接受到响应的时候 告诉系统如何处理服务器返回的数据
completionHandler(NSURLSessionResponseAllow);
//得到请求文件的数据大小
self.totalLength = response.expectedContentLength;
//拼接文件的全路径
NSString fileName = response.suggestedFilename;
NSString cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString fullPath = [cachePath stringByAppendingPathComponent:fileName];1
2
3//(1)创建输出流,并打开
self.outStream = [[NSOutputStream alloc] initToFileAtPath:fullPath append:YES];
[self.outStream open];}
-(void)URLSession:(NSURLSession )session dataTask:(NSURLSessionDataTask )dataTask didReceiveData:(NSData )data {
//(2)使用输出流写数据
[self.outStream write:data.bytes maxLength:data.length];
//累加已经下载的文件数据大小
self.currentLength += data.length;
//计算文件的下载进度 = 已经下载的 / 文件的总大小
self.progressView.progress = 1.0 self.currentLength / self.totalLength;
}
-(void)URLSession:(NSURLSession )session task:(NSURLSessionTask )task didCompleteWithError:(NSError *)error {
//(3)关闭输出流
[self.outStream close];
}1
2

解决内存飙升
4. 常用操作:开始 | 暂停 | 取消 | 恢复
对于下载状态的控制(dataTask 为定义的下载任务属性,将创建任务的代码写到懒加载中)
开始下载
1
2
[self.dataTask resume];
1
2
3
4- 暂停下载
```objective-c
[self.dataTask suspend];恢复下载
1
2
[self.dataTask resume];
1
2
3
4
5
6- 取消下载
```objective-c
[self.dataTask cancel];
//默认情况下取消下载不能进行恢复,若要取消之后还可以恢复,可以清空下载任务,再新建
self.dataTask = nil;

下载控制效果
5. 断点下载
在上面的效果图中,我们已经看到可以控制下载的状态,但是到最后又有了一个新的问题:下载进度值发生跳跃错乱
原因分析:在前面计算进度值的时候,我们一直使用的方法是用已经下载的数据 / 文件的总数据,在第一个代理方法中,我们得到的文件大小并不是真正的文件大小,而是剩余未下载的大小,所以在第一次开始下载时,可以得到正确的数据,但是在下载过程中执行其他操作,就会使得到的数据大小发生变化,从而导致下载进度值出现问题
解决方案:文件总大小 = 已经下载的数据 + 剩余未下载的数据
1
2
self.totalLength = response.expectedContentLength + self.currentLength;
1 | - 优化性能(以文件句柄方式为例,输出流同理):只有第一次接收到响应的时候才需要创建空的文件 |
实现断点续传
在创建文件句柄后,更改文件句柄指向文件的末尾
1
2
[self.handle seekToEndOfFile];
1
2
3
4
52. 在请求头信息中添加需要请求的数据范围(从当前已经下载的数据末尾开始,到整个文件的末尾)
```objective-c
NSString *rangeString = [NSString stringWithFormat:@"bytes=%zd-",self.currentLength];
[request setValue:rangeString forHTTPHeaderField:@"Range"];

断点下载效果
6. 离线断点下载
在用户日常使用的过程中,可能会出现下载文件到一半的时候,网络断开导致下载失败,为了避免重复下载,就要用到离线下载的功能
做离线断点下载的主要步骤就是要到沙盒中获取到之前已经下载好的数据和数据的大小,因此发送请求开始下载之前,要先在 viewDidLoad 中做一些处理
1
2
//获得之前已经下载的文件数据大小 => 获得沙盒中已经存在的文件数据大小
//获得某个路径对应文件的属性
NSDictionary *fileInfo = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];
self.currentLength = [fileInfo fileSize];
1 | - 此时离线断点下载的功能也已经做好,但是仍有一些小问题需要处理 |
2. 在 viewDidLoad 中做处理
1
2
3
4
5
6
//显示文件的进度信息 = 已经下载文件数据大小(self.currentLength) / 文件的总大小
NSData *totalSize = [NSData dataWithContentsOfFile:SizefullPath];
self.totalLength = [[[NSString alloc]initWithData:totalSize encoding:NSUTF8StringEncoding] integerValue];
if (self.totalLength != 0) {
self.progressView.progress = 1.0 * self.currentLength/self.totalLength;
}

写在最后
以上就是使用 NSURLSession 实现离线断点下载的全部过程,由于个人水平有限,如有错误,敬请指正!如果觉得这篇文章对您有所帮助,请点击下方的喜欢或关注本人,谢谢您的支持!